package org.xi.maple.util;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;


public final class MapleServiceLoader {

    private static final String PREFIX = "META-INF/provides/";

    public static <S> Map<String, Class<?>> load(Class<S> svc, ClassLoader cl) {
        Map<String, Class<?>> providers = new HashMap<>();
        Class<S> service = Objects.requireNonNull(svc, "Service interface cannot be null");
        ClassLoader loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;

        try {
            String fullName = PREFIX + service.getName();
            Enumeration<URL> configs = loader == null ? ClassLoader.getSystemResources(fullName) : loader.getResources(fullName);
            while (configs.hasMoreElements()) {
                parse(service, loader, configs.nextElement(), providers);
            }
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
        return providers;
    }

    public static <S> Map<String, Class<?>> load(Class<S> svc) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return load(svc, cl);
    }

    public static <S> Map<String, Class<?>> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return load(service, prev);
    }

    private static int parseLine(Class<?> service, ClassLoader loader, URL u, BufferedReader r, int lc, Map<String, Class<?>> providers) throws IOException, ServiceConfigurationError {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }

        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();

        String[] namePair = ln.split("=");
        String ck = namePair[0];
        String cn = namePair[1];

        if (namePair.length != 2) {
            fail(service, "Provider " + ln + " format error");
        } else if (providers.containsKey(ck)) {
            fail(service, "Provider " + ln + " is duplicate");
        }

        int n = cn.length();
        if (n != 0) {
            if ((cn.indexOf(' ') >= 0) || (cn.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = cn.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + cn);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = cn.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + cn);
            }
            putNextService(service, loader, namePair, providers);
        }
        return lc + 1;
    }

    private static void parse(Class<?> service, ClassLoader loader, URL u, Map<String, Class<?>> providers) throws ServiceConfigurationError {
        InputStream in = null;
        BufferedReader r = null;
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
            int lc = 1;
            while ((lc = parseLine(service, loader, u, r, lc, providers)) >= 0) ;
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
    }

    private static void putNextService(Class<?> service, ClassLoader loader, String[] namePair, Map<String, Class<?>> providers) {
        Class<?> c = null;
        try {
            c = Class.forName(namePair[1], false, loader);
        } catch (ClassNotFoundException x) {
            fail(service, "Provider " + namePair[1] + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service, "Provider " + namePair[1] + " not a subtype");
        } else {
            providers.put(namePair[0], c);
        }
    }

    private static void fail(Class<?> service, String msg, Throwable cause) throws ServiceConfigurationError {
        throw new ServiceConfigurationError(service.getName() + ": " + msg, cause);
    }

    private static void fail(Class<?> service, String msg) throws ServiceConfigurationError {
        throw new ServiceConfigurationError(service.getName() + ": " + msg);
    }

    private static void fail(Class<?> service, URL u, int line, String msg) throws ServiceConfigurationError {
        fail(service, u + ":" + line + ": " + msg);
    }
}
